#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <ipms.h>
#include <pcd_api.h>
#include "ipms_dicop.h"

struct dicop_app *AppIPMS;
struct dicop_signal_desc *SigStateChange;
struct dicop_signal_desc *SigAutoReport;

static queue_t *ReportQueue;
static queue_t *MACQueue;
static queue_t *StatusQueue;

static callback_t MethodQueryID(callback_args_t args)
{
    char mac[24] = {0};
    struct pcd_struct *PCDInfo;
    char *StrID;

    dicop_callback_get_args(args, 1, "MAC", mac);

    PCDInfo = pcd_list_search_by_mac(mac);
    if (!PCDInfo)
        StrID = "-1";
    else
        StrID = itoa(PCDInfo->id);

    return dicop_callback_put_reply(1, "ID", StrID);
}

static callback_t MethodRequestReport(callback_args_t args)
{
    char id_str[8] = {0};
    struct pcd_struct PCDInfo;
#ifdef WIN32
    DWORD timeout = 5000;
    DWORD status;
#else
    struct timespec timeout = { time(NULL) + 5, 0 };
    int status;
#endif
    int timeout_expired = 0;

#ifdef WIN32
    PCDInfo.semaphore = CreateSemaphore(NULL, 0, 1, NULL);
    if(PCDInfo.semaphore == (HANDLE) NULL) {
        PRINT_ERROR("Unable to create PCDInfo semaphore\n");
        return dicop_callback_null_reply();
    }
#else
    sem_init(&PCDInfo.semaphore, 0, 0);
#endif

    dicop_callback_get_args(args, 1, "ID", id_str);
    PCDInfo.id = (u_int16)atoi(id_str);

    pcd_report_request(PCDInfo.id);

    queue_push(ReportQueue, &PCDInfo);

#ifdef WIN32
    status = WaitForSingleObject(PCDInfo.semaphore, timeout);
    if (status == WAIT_TIMEOUT)
        timeout_expired = 1;
#else
    status = sem_timedwait(&PCDInfo.semaphore, &timeout);
    if (status < 0)
        timeout_expired = 1;
#endif

    if (timeout_expired) {
        PRINT_WARNING("Method reply timeout!\n");
        return dicop_callback_null_reply();
    }

    return dicop_callback_put_reply(6, "ID", itoa(PCDInfo.id),
                                       "Datetime", PCDInfo.last_update,
                                       "Voltage", itoa(PCDInfo.reports.voltage),
                                       "Current", itoa(PCDInfo.reports.current),
                                       "InstantPower",  itoa(PCDInfo.reports.instant_power),
                                       "AccumPower", itoa(PCDInfo.reports.accum_power));
}

static callback_t MethodSetAutoReportTimer(callback_args_t args)
{
    char id_str[8] = {0};
    char time_str[20] = {0};
    u_int16 id;
    u_int16 time;

    dicop_callback_get_args(args, 2, "ID", &id_str, "Time", &time_str);
    id = (u_int16)atoi(id_str);
    time = (u_int16)atoi(time_str);

    pcd_set_autoreport_timer(id, time);
    return 0;
}

static callback_t MethodSelectReportType(callback_args_t args)
{
    char id_str[8] = {0};
    char selection_str[8] = {0};
    u_int16 id;
    u_int16 selection;

    dicop_callback_get_args(args, 2, "ID", id_str, "Selection", selection_str);
    id = (u_int16)atoi(id_str);
    selection = (u_int16)atoi(selection_str);

    pcd_report_type_select(id, selection);
    return 0;
}

static callback_t MethodSetTurnOnTimer(callback_args_t args)
{
    char id_str[8] = {0};
    char time_str[20] = {0};
    u_int16 id;
    u_int16 time;

    dicop_callback_get_args(args, 2, "ID", id_str, "Time", time_str);
    id = (u_int16)atoi(id_str);
    time = (u_int16)atoi(time_str);

    pcd_set_turnon_timer(id, time);
    return 0;
}

static callback_t MethodSetTurnOffTimer(callback_args_t args)
{
    char id_str[8] = {0};
    char time_str[20] = {0};
    u_int16 id;
    u_int16 time;

    dicop_callback_get_args(args, 2, "ID", id_str, "Time", time_str);
    id = (u_int16)atoi(id_str);
    time = (u_int16)atoi(time_str);

    pcd_set_turnoff_timer(id, time);
    return 0;
}

static callback_t MethodRequestMAC(callback_args_t args)
{
    char id_str[8] = {0};
    char mac[24] = {0};
    struct pcd_struct PCDInfo;
#ifdef WIN32
    DWORD timeout = 5000;
    DWORD status;
#else
    struct timespec timeout = { time(NULL) + 5, 0 };
    int status;
#endif
    int timeout_expired = 0;

#ifdef WIN32
    PCDInfo.semaphore = CreateSemaphore(NULL, 0, 1, NULL);
    if(PCDInfo.semaphore == (HANDLE) NULL) {
        PRINT_ERROR("Unable to create PCDInfo semaphore\n");
        return dicop_callback_null_reply();
    }
#else
    sem_init(&PCDInfo.semaphore, 0, 0);
#endif

    dicop_callback_get_args(args, 1, "ID", id_str);
    PCDInfo.id = (u_int16)atoi(id_str);

    pcd_mac_request(PCDInfo.id);

    queue_push(MACQueue, &PCDInfo);

#ifdef WIN32
    status = WaitForSingleObject(PCDInfo.semaphore, timeout);
    if (status == WAIT_TIMEOUT)
        timeout_expired = 1;
#else
    status = sem_timedwait(&PCDInfo.semaphore, &timeout);
    if (status < 0)
        timeout_expired = 1;
#endif

    if (timeout_expired) {
        PRINT_WARNING("Method reply timeout!\n");
        return dicop_callback_null_reply();
    }

    strncpy(mac, PCDInfo.mac_addr, 24);

    return dicop_callback_put_reply(1, "MAC", mac);
}

static callback_t MethodRequestStatus(callback_args_t args)
{
    char id_str[8] = {0};
    struct pcd_struct PCDInfo;
#ifdef WIN32
    DWORD timeout = 5000;
    DWORD status;
#else
    struct timespec timeout = { time(NULL) + 5, 0 };
    int status;
#endif
    int timeout_expired = 0;

#ifdef WIN32
    PCDInfo.semaphore = CreateSemaphore(NULL, 0, 1, NULL);
    if(PCDInfo.semaphore == (HANDLE) NULL) {
        PRINT_ERROR("Unable to create PCDInfo semaphore\n");
        return dicop_callback_null_reply();
    }
#else
    sem_init(&PCDInfo.semaphore, 0, 0);
#endif

    dicop_callback_get_args(args, 1, "ID", id_str);
    PCDInfo.id = (u_int16)atoi(id_str);

    pcd_status_request(PCDInfo.id);

    queue_push(StatusQueue, &PCDInfo);

#ifdef WIN32
    status = WaitForSingleObject(PCDInfo.semaphore, timeout);
    if (status == WAIT_TIMEOUT)
        timeout_expired = 1;
#else
    status = sem_timedwait(&PCDInfo.semaphore, &timeout);
    if (status < 0)
        timeout_expired = 1;
#endif

    if (timeout_expired) {
        PRINT_WARNING("Method reply timeout!\n");
        return dicop_callback_null_reply();
    }

    return dicop_callback_put_reply(3, "ID", itoa(PCDInfo.id),
                                       "TimeLeft", itoa(PCDInfo.time_left),
                                       "Status", itoa(PCDInfo.status));
}


/* IPMS Callbacks */
void DicopJoinCallback(struct pcd_struct *PCDListNode)
{
    PRINT_DEBUG("[JOIN][%s] ID=%d MAC=%s\n",
                PCDListNode->last_update, PCDListNode->id, PCDListNode->mac_addr);
}

void DicopReportCallback(struct pcd_struct *PCDListNode)
{
    struct pcd_struct *PCDInfo;
    int QIndex;

    PRINT_DEBUG("[REPORT][%s] ID=%d Volt=%d Curr=%d InstantPower=%d AccumPower=%d\n",
                PCDListNode->last_update, PCDListNode->id,
                PCDListNode->reports.voltage, PCDListNode->reports.current,
                PCDListNode->reports.instant_power, PCDListNode->reports.accum_power);

    if (!queue_is_empty(ReportQueue)) {
        for (QIndex = 1; QIndex <= ReportQueue->count; QIndex++) {
            PCDInfo = (struct pcd_struct *)queue_peek_nth(ReportQueue, QIndex);
            if (PCDInfo->id == PCDInfo->id) {
                PCDInfo = (struct pcd_struct *)queue_steal_nth(ReportQueue, QIndex);

                PCDInfo->reports = PCDListNode->reports;

                strncpy(PCDInfo->last_update, PCDListNode->last_update, 20);
#ifdef WIN32
                ReleaseSemaphore(PCDInfo->semaphore, 1, NULL);
#else
                sem_post(&PCDInfo->semaphore);
#endif
                break;
            }
        }
    }
}

void DicopMACCallback(struct pcd_struct *PCDListNode)
{
    struct pcd_struct *PCDInfo;
    int QIndex;

    PRINT_DEBUG("[MAC][%s] ID=%d MAC=%s\n",
                PCDListNode->last_update, PCDListNode->id, PCDListNode->mac_addr);

    if (!queue_is_empty(MACQueue)) {
        for (QIndex = 1; QIndex <= MACQueue->count; QIndex++) {
            PCDInfo = (struct pcd_struct *)queue_peek_nth(MACQueue, QIndex);
            if (PCDInfo->id == PCDListNode->id) {
                PCDInfo = (struct pcd_struct *)queue_steal_nth(MACQueue, QIndex);

                strncpy(PCDInfo->mac_addr, PCDListNode->mac_addr, 24);
                strncpy(PCDInfo->last_update, PCDListNode->last_update, 20);
#ifdef WIN32
                ReleaseSemaphore(PCDInfo->semaphore, 1, NULL);
#else
                sem_post(&PCDInfo->semaphore);
#endif
                break;
            }
        }
    }
}

void DicopStatusCallback(struct pcd_struct *PCDListNode)
{
    struct pcd_struct *PCDInfo;
    int QIndex;

    PRINT_DEBUG("[STATUS][%s] ID=%d TimeLeft=%d Status=%d\n",
                PCDListNode->last_update, PCDListNode->id,
                PCDListNode->time_left, PCDListNode->status);

    if (!queue_is_empty(StatusQueue)) {
        for (QIndex = 1; QIndex <= StatusQueue->count; QIndex++) {
            PCDInfo = (struct pcd_struct *)queue_peek_nth(StatusQueue, QIndex);
            if (PCDInfo->id == PCDListNode->id) {
                PCDInfo = (struct pcd_struct *)queue_steal_nth(StatusQueue, QIndex);

                PCDInfo->time_left = PCDListNode->time_left;
                PCDInfo->status = PCDListNode->status;

                strncpy(PCDInfo->last_update, PCDListNode->last_update, 20);
#ifdef WIN32
                ReleaseSemaphore(PCDInfo->semaphore, 1, NULL);
#else
                sem_post(&PCDInfo->semaphore);
#endif
                break;
            }
        }
    }
}

void DicopEventProcess(struct pcd_struct *PCDInfo, u_int8 EventType)
{
    if (!PCDInfo)
        return;

    switch (EventType) {
        case RCMD_EVENT:
            PRINT_DEBUG("[CHANGE][%s] ID=%d Event=%d\n",
                        PCDInfo->last_update, PCDInfo->id, PCDInfo->event);

            dicop_signal_emit(SigStateChange,
                              "ID", itoa(PCDInfo->id),
                              "Datetime", PCDInfo->last_update,
                              "Event", itoa(PCDInfo->event));
            break;
        case RCMD_AUTO_REPORT:
            PRINT_DEBUG("[AUTOREPORT][%s] ID=%d Volt=%d Curr=%d InstantPower=%d AccumPower=%d\n",
                        PCDInfo->last_update, PCDInfo->id,
                        PCDInfo->reports.voltage, PCDInfo->reports.current,
                        PCDInfo->reports.instant_power, PCDInfo->reports.accum_power);

            dicop_signal_emit(SigAutoReport,
                              "ID", itoa(PCDInfo->id),
                              "Datetime", PCDInfo->last_update,
                              "Voltage", itoa(PCDInfo->reports.voltage),
                              "Current", itoa(PCDInfo->reports.current),
                              "InstantPower", itoa(PCDInfo->reports.instant_power),
                              "AccumPower", itoa(PCDInfo->reports.accum_power));
            break;
    }
}

int DicopIPMSInit(void)
{
    struct dicop_method_desc *MethQueryID;
    struct dicop_method_desc *MethReqReport;
    struct dicop_method_desc *MethSetAutoRepTimer;
    struct dicop_method_desc *MethSelRepType;
    struct dicop_method_desc *MethSetTurnOnTimer;
    struct dicop_method_desc *MethSetTurnOffTimer;
    struct dicop_method_desc *MethReqMAC;
    struct dicop_method_desc *MethReqStatus;

    dicop_init();

    AppIPMS = dicop_app_new("IPMS");
    if (!AppIPMS) {
        printf("ERROR: Unable to create app\n");
        return -1;
    }

    /* Methods */
    MethQueryID = dicop_method_new(AppIPMS, "QueryID");
    dicop_method_args_init(MethQueryID, 1, "MAC");
    dicop_method_reply_init(MethQueryID, 1, "ID");
    dicop_method_callback_install(MethQueryID, MethodQueryID);

    MethReqReport = dicop_method_new(AppIPMS, "RequestReport");
    dicop_method_args_init(MethReqReport, 1, "ID");
    dicop_method_reply_init(MethReqReport, 6, "ID", "Datetime", "Voltage", "Current", "InstantPower", "AccumPower");
    dicop_method_callback_install(MethReqReport, MethodRequestReport);

    MethSetAutoRepTimer = dicop_method_new(AppIPMS, "SetAutoReportTimer");
    dicop_method_args_init(MethSetAutoRepTimer, 2, "ID", "Time");
    dicop_method_callback_install(MethSetAutoRepTimer, MethodSetAutoReportTimer);

    MethSelRepType = dicop_method_new(AppIPMS, "SelectReportType");
    dicop_method_args_init(MethSelRepType, 2, "ID", "Selection");
    dicop_method_callback_install(MethSelRepType, MethodSelectReportType);

    MethSetTurnOnTimer = dicop_method_new(AppIPMS, "SetTurnOnTimer");
    dicop_method_args_init(MethSetTurnOnTimer, 2, "ID", "Time");
    dicop_method_callback_install(MethSetTurnOnTimer, MethodSetTurnOnTimer);

    MethSetTurnOffTimer = dicop_method_new(AppIPMS, "SetTurnOffTimer");
    dicop_method_args_init(MethSetTurnOffTimer, 2, "ID", "Time");
    dicop_method_callback_install(MethSetTurnOffTimer, MethodSetTurnOffTimer);

    MethReqMAC = dicop_method_new(AppIPMS, "RequestMAC");
    dicop_method_args_init(MethReqMAC, 1, "ID");
    dicop_method_reply_init(MethReqMAC, 1, "MAC");
    dicop_method_callback_install(MethReqMAC, MethodRequestMAC);

    MethReqStatus = dicop_method_new(AppIPMS, "RequestStatus");
    dicop_method_args_init(MethReqStatus, 1, "ID");
    dicop_method_reply_init(MethReqStatus, 3, "ID", "TimeLeft", "Status");
    dicop_method_callback_install(MethReqStatus, MethodRequestStatus);

    /* Signals */
    SigStateChange = dicop_signal_new(AppIPMS, "StateChange");
    dicop_signal_args_init(SigStateChange, 3, "ID", "Datetime", "Event");

    SigAutoReport = dicop_signal_new(AppIPMS, "AutoReport");
    dicop_signal_args_init(SigAutoReport, 6, "ID", "Datetime", "Voltage", "Current", "InstantPower", "AccumPower");

    dicop_server_create(12250);

    ReportQueue = queue_create();
    MACQueue = queue_create();
    StatusQueue = queue_create();

    return 0;
}

void DicopIPMSExit(void)
{
    queue_destroy(ReportQueue);
    queue_destroy(MACQueue);
    queue_destroy(StatusQueue);

    dicop_app_destroy(AppIPMS);
    dicop_server_terminate();
}
